// 3D model
function Model3DViewer(canvas, model) {
    var gl = null;
    var projectionMatrix = null;
    var modelViewMatrix = null;
    var shaderVertexPositionAttribute = null;
    var shaderProjectionMatrixUniform = null;
    var shaderModelViewMatrixUniform = null;
    var patchColorUniform = null;

    var patchVertexBuffer = null;
    var patchIndexBuffer = null;

    var selectedPoints = [];

    var modelPatches = null;

    function initializeWebGL() {
        try 
        {
            gl = canvas.getContext("experimental-webgl");
        } 
        catch (e)
        {
            var msg = "Error creating WebGL context: " + e.toString();
            throw Error(msg);
        }      
    }

    function initializeViewport() {
/*
        // hack for anti-aliasing
        canvas.width *= 2;
        canvas.height *= 2;
        canvas.style.width = canvas.width / 2;
        canvas.style.height = canvas.height / 2;
*/
        gl.viewport(0, 0, canvas.width, canvas.height);
    }

    function initializeModelViewMatrix (argument) {
        // The transform matrix (translate back in Z for the camera)
        modelViewMatrix = new Float32Array([
            1, 0, 0, 0,
            0, 1, 0, 0, 
            0, 0, 1, 0, 
            0, 0, -3.333, 1]);
        var normalizer = 1.0 / Math.max(model.width, model.height);
        var scale = vec3.fromValues(normalizer, normalizer, normalizer);
        mat4.scale(modelViewMatrix, modelViewMatrix, scale);
    }

    function initializeProjectionMatrix(argument) {
        // The projection matrix (for a 45 degree field of view)
        var ratio = canvas.width / canvas.height;
        projectionMatrix = mat4.create();
        mat4.perspective(projectionMatrix, 45, ratio, 0.1, 100);
    }

    function initializeMatrices() {
        initializeModelViewMatrix();
        initializeProjectionMatrix();
    }

    function getShader(id) {
        var shaderScript = document.getElementById(id);
        if (!shaderScript) {
            return null;
        }
        var source = "";
        var node = shaderScript.firstChild;
        while (node) {
            if (node.nodeType == 3) {
                source += node.textContent;
            }
            node = node.nextSibling;
        }
        var shader = null;
        if (shaderScript.type == "x-shader/x-fragment") {
            shader = gl.createShader(gl.FRAGMENT_SHADER);
        }
        else if (shaderScript.type == "x-shader/x-vertex") {
            shader = gl.createShader(gl.VERTEX_SHADER);
        }
        else {
            return null;
        }
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            throw Error(gl.getShaderInfoLog(shader));
        }
        return shader;
    }

    function initializeShader() {
        // load and compile the fragment and vertex shader
        var fragmentShader = getShader("shader-fs");
        var vertexShader = getShader("shader-vs");

        // link them together into a new program
        var shaderProgram = gl.createProgram();
        gl.attachShader(shaderProgram, vertexShader);
        gl.attachShader(shaderProgram, fragmentShader);
        gl.linkProgram(shaderProgram);

        // get pointers to the shader params
        shaderVertexPositionAttribute = gl.getAttribLocation(shaderProgram, "vertexPos");
        gl.enableVertexAttribArray(shaderVertexPositionAttribute);
        
        shaderProjectionMatrixUniform = gl.getUniformLocation(shaderProgram, "projectionMatrix");
        shaderModelViewMatrixUniform = gl.getUniformLocation(shaderProgram, "modelViewMatrix");
        patchColorUniform = gl.getUniformLocation(shaderProgram, "uColor");

        if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
            throw Error("Error initializing shaders");
        }

        // set the shader to use
        gl.useProgram(shaderProgram);

        gl.disable(gl.DEPTH_TEST);
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
    }

    function initializeBuffers() {
        patchVertexBuffer = gl.createBuffer();
        patchIndexBuffer = gl.createBuffer();
    }

    function draw() {
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);

        gl.bindBuffer(gl.ARRAY_BUFFER, patchVertexBuffer);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, patchIndexBuffer);

        gl.vertexAttribPointer(shaderVertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
        gl.uniformMatrix4fv(shaderProjectionMatrixUniform, false, projectionMatrix);
        gl.uniformMatrix4fv(shaderModelViewMatrixUniform, false, modelViewMatrix);

        if (selectedPoints.length > 0) {
            gl.uniform4f(patchColorUniform, 1.0, 0.0, 0.0, 1.0);
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(selectedPoints), gl.STATIC_DRAW);
            gl.drawArrays(gl.POINTS, 0, selectedPoints.length / 3);
        }

        var patches = modelPatches;

        for (var i = 0; i < patches.length; ++i) {
            var color = patches[i].color;
            gl.uniform4f(patchColorUniform, color[0], color[1], color[2], color[3]);
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(patches[i].vertices), gl.STATIC_DRAW);
            gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(patches[i].indices), gl.STATIC_DRAW);
            gl.drawElements(gl.TRIANGLES, patches[i].indices.length, gl.UNSIGNED_SHORT, 0);
        }
    }

    function initializeEventListener() {
        var isMouseDown = false;
        var x = null;
        var y = null;

        function mouseDown (event) {
            isMouseDown = true;
            x = event.clientX;
            y = event.clientY;
        }

        function mouseUp (event) {
            isMouseDown = false;
        }

        function mouseMove (event) {
            if (!isMouseDown) {
                return;
            }
            var newX = event.clientX;
            var newY = event.clientY;
            var deltaX = newX - x;
            var deltaY = newY - y;

            var rotation = mat4.create();
            mat4.identity(rotation);
            mat4.rotate(rotation, rotation, glMatrix.toRadian(deltaX / 2), [0, 1, 0]);
            mat4.rotate(rotation, rotation, glMatrix.toRadian(deltaY / 2), [1, 0, 0]);
            mat4.multiply(modelViewMatrix, modelViewMatrix, rotation);

            x = newX;
            y = newY;
        }

        var zooming = vec3.create();
        var zoomingLevel = 0;
        function scroll(event) {
            var delta = event.wheelDelta || -event.detail;
            if (delta < 0) {
                if (zoomingLevel <= -10) {
                    return;
                }
                --zoomingLevel;
                vec3.set(zooming, 0.8, 0.8, 0.8);
                mat4.scale(modelViewMatrix, modelViewMatrix, zooming);
            }
            else {
                if (zoomingLevel >= 10) {
                    return;
                }
                ++zoomingLevel;
                vec3.set(zooming, 1.25, 1.25, 1.25);
                mat4.scale(modelViewMatrix, modelViewMatrix, zooming);
            }
        }

        function keyDown(event) {
            var code = event.keyCode;
            var offset;
            if (code == 65) { // A (left)
                offset = vec3.fromValues(-0.1, 0, 0);
            }
            else if (code == 68) { // D (right)
                offset = vec3.fromValues(0.1, 0, 0);
            }
            else if (code == 87) { // W (up)
                offset = vec3.fromValues(0, 0.1, 0);
            }
            else if (code == 83) { // S (down)
                offset = vec3.fromValues(0, -0.1, 0);
            }
            else {
                return;
            }
            mat4.translate(modelViewMatrix, modelViewMatrix, offset);
        }

        canvas.addEventListener("mousedown", mouseDown);
        document.addEventListener("mouseup", mouseUp);
        document.addEventListener("mousemove", mouseMove);
        window.addEventListener("mousewheel", scroll);
        window.addEventListener("DOMMouseScroll", scroll); // Firefox compatibility
        document.addEventListener("keydown", keyDown);
    }

    function initialize() {
        initializeWebGL();
        initializeViewport();
        initializeMatrices();
        initializeShader();
        initializeBuffers();
        initializeEventListener();
    }

    this.start = function() {
        var caller = function (timestamp) {
            window.requestAnimationFrame(caller);
            draw();
        };
        caller();
    };

    this.refreshModel = function() {
        modelPatches = createModelVertices(model);
    };

    this.addSelectedPoint = function(point) {
        selectedPoints.push(point.x);
        selectedPoints.push(point.y);
        selectedPoints.push(point.z);
    };

    this.removeAllSelectedPoints = function() {
        selectedPoints.length = 0;
    };

    this.resetViewport = function(width, height) {
        canvas.width = width;
        canvas.height = height;
        initializeViewport();
        initializeProjectionMatrix();
    };

    initialize();
    this.refreshModel();
}

// 2D model
function CreasePatternViewer(canvas, model) {
    var context = canvas.getContext("2d");

    var offset = 10;
    var scale = 1.0;

    this.setSize = function(maxWidth, maxHeight) {
        var scaleX = (maxWidth - offset * 2) / model.width;
        var scaleY = (maxHeight - offset * 2) / model.height;
        if (scaleX < scaleY) {
            scale = scaleX;
            canvas.width = maxWidth;
            canvas.height = scale * model.height + offset * 2;
        }
        else {
            scale = scaleY;
            canvas.width = scale * model.width + offset * 2;
            canvas.height = maxHeight;
        }
        context.setTransform(1, 0, 0, 1, 0, 0);
        context.translate(offset, offset);
        context.scale(scale, scale);
    };

    this.setSize(canvas.width, canvas.height);

    var selectedPatch = null;
    var selectedPoints = [];
    var selectedEdges = [];
    var creasePreview = {};
    
    var foldingTool = new DummyFoldingTool();
    var isSmartSelection = true;

    var onPointTouched = function(point, patch) { };
    var onCreaseCreated = function(start, end, patch) { };

    function DummyFoldingTool() {
        this.previewFocus = function(point, patch) {
            return false;
        };

        this.addFocus = function(point, patch) {
            // do nothing
        };
    }

    function LineFoldingTool() {
        this.previewFocus = function(point, patch) {
            creasePreview = {};
            var element = findHitEdge(point.x, point.y, patch, 10);
            if (!element) {
                return false;
            }
            if (selectedEdges.length == 0 || selectedPatch != patch) {
                return true;
            }
            creasePreview = edgesBisector(selectedEdges[0], element);
            return true;
        };

        this.addFocus = function(point, patch) {
            var element = findHitEdge(point.x, point.y, patch, 10);
            if (!element) {
                return;
            }
            if (selectedPatch != patch) {
                clearSelection();
                selectedPatch = patch;
            }
            selectedEdges.push(element);
            tryNotifyCreaseCreated();
        };
    }

    function PointFoldingTool() {
        this.previewFocus = function(point, patch) {
            creasePreview = {};
            var element = findFocusedPoint(point, patch);
            if (!element) {
                return false;
            }
            if (selectedPoints.length == 0 || selectedPatch != patch) {
                return true;
            }
            var x1 = selectedPoints[0].x;
            var y1 = selectedPoints[0].y;
            var x2 = element.x;
            var y2 = element.y;
            creasePreview = perpendicularBisector(x1, y1, x2, y2);
            return true;
        };

        this.addFocus = function(point, patch) {
            var element = findFocusedPoint(point, patch);
            if (!element) {
                return;
            }
            if (selectedPatch != patch) {
                clearSelection();
                selectedPatch = patch;
            }
            selectedPoints.push(element);
            tryNotifyCreaseCreated();
        };
    }

    function CreaseCreatingTool() {
        this.previewFocus = function(point, patch) {
            creasePreview = {};
            var element = findFocusedPoint(point, patch);
            if (!element) {
                return false;
            }
            if (selectedPoints.length == 0 || selectedPatch != patch) {
                return true;
            }
            creasePreview.start = selectedPoints[0];
            creasePreview.end = element;
            return true;
        };

        this.addFocus = function(point, patch) {
            var element = findFocusedPoint(point, patch);
            if (!element) {
                return;
            }
            if (selectedPatch != patch) {
                clearSelection();
                selectedPatch = patch;
            }
            selectedPoints.push(element);
            tryNotifyCreaseCreated();
        };
    }

    function tryNotifyCreaseCreated() {
        if (creasePreview.start && creasePreview.end) {
            onCreaseCreated(creasePreview.start, creasePreview.end, selectedPatch);
            clearSelection();
        }
    }

    function findFocusedPoint(point, patch) {
        if (!isSmartSelection) {
            return point;
        }
        return findHitVertex(point.x, point.y, patch, 20);
    }

    function drawPatch(patch) {
        var pointCount = patch.data.length / 2;
        var last = pointCount - 1;
        if (last < 0) {
            return;
        }
        context.beginPath();
        context.moveTo(patch.data[last*2], patch.data[last*2+1]);
        for (var i = 0; i < pointCount; i++) {
            context.lineTo(patch.data[i*2], patch.data[i*2+1]);
        }
        context.closePath();
    }

    function drawPoint(point) {
        var size = 2 / scale;
        context.fillRect(point.x - size, point.y - size, size * 2, size * 2);
    }

    function drawLine(start, end) {
        context.beginPath();
        context.moveTo(start.x, start.y);
        context.lineTo(end.x, end.y);
        context.closePath();
        context.stroke();
    }

    function clearSelection() {
        selectedPatch = null;
        selectedPoints.length = 0;
        selectedEdges.length = 0;
        creasePreview = {};
    }

    function initializeEventListener(viewer) {

        function hitTest(clientX, clientY) {
            var bound = canvas.getBoundingClientRect();
            var x = clientX - bound.left;
            var y = clientY - bound.top;
            // tricky: must apply the reverse transform of that used when paper is drawn on canvas
            x = (x - offset) / scale;
            y = (y - offset) / scale;
            for (var i = 0; i < model.patches.length; i++) {
                var patch = model.patches[i];
                if (isPointInPatch(x, y, patch)) {
                    return { point: { x: x, y: y, z: 0 }, patch: patch };
                }
            }
            return null;
        }

        function onMouseMove(event) {
            var focus = hitTest(event.clientX, event.clientY);
            var enabled = false;
            if (focus) {
                onPointTouched(focus.point, focus.patch);
                enabled = foldingTool.previewFocus(focus.point, focus.patch);
            }
            else {
                if (creasePreview.start || creasePreview.end) {
                    creasePreview = {};
                }
            }
            canvas.style.cursor = enabled ? "crosshair" : "default";
            viewer.draw();
        }

        function onMouseDown(event) {
            var focus = hitTest(event.clientX, event.clientY);
            if (focus) {
                foldingTool.addFocus(focus.point, focus.patch);
            }
            else {
                clearSelection();
            }
            viewer.draw();
        }

        document.addEventListener("mousemove", onMouseMove);
        canvas.addEventListener("mousedown", onMouseDown);
    }

    this.draw = function() {
        context.save();
        context.setTransform(1, 0, 0, 1, 0, 0);
        context.clearRect(0, 0, canvas.width, canvas.height);
        context.restore();

		context.fillStyle = "transparent";
        context.strokeStyle = "rgba(255, 255, 255, 0.25)";
        context.lineWidth = 1 / scale;
        for (var i = 0; i < model.patches.length; i++) {
            drawPatch(model.patches[i]);
            context.stroke();
        }

        if (selectedPatch) {
            context.fillStyle = "rgba(255, 0, 0, 0.1)";
            drawPatch(selectedPatch);
            context.fill();
        }

        context.strokeStyle = "rgba(255, 255, 255, 0.75)";
        context.fillStyle = "rgba(255, 255, 255, 0.05";
        context.lineWidth = 3 / scale;
        context.fillRect(0, 0, model.width, model.height);

        if (selectedPoints) {
            context.fillStyle = "#f00";
            for (var i = 0; i < selectedPoints.length; i++) {
                drawPoint(selectedPoints[i]);
            }
        }

        if (selectedEdges) {
            context.strokeStyle = "#f00";
            context.lineWidth = 3 / scale;
            for (var i = 0; i < selectedEdges.length; i++) {
                drawLine(selectedEdges[i].start, selectedEdges[i].end);
            }
        }

        if (creasePreview.start && creasePreview.end) {
            context.strokeStyle = "#f00";
            context.lineWidth = 1 / scale;
            drawLine(creasePreview.start, creasePreview.end);
        }
    };

    this.pointTouched = function(callback) {
        onPointTouched = callback;
    };

    this.creaseCreated = function(callback) {
        onCreaseCreated = callback;
    };

    this.setFoldingTool = function(type) {
        if (type === this.FOLD_NONE) {
            foldingTool = new DummyFoldingTool();
        }
        else if (type === this.FOLD_LINES) {
            foldingTool = new LineFoldingTool();
        }
        else if (type === this.FOLD_POINTS) {
            foldingTool = new PointFoldingTool();
        }
        else if (type === this.CREATE_CREASE) {
            foldingTool = new CreaseCreatingTool();
        }
        else {
            throw Error("Invalid folding tool");
        }
    }

    this.setSmartSelection = function(smart) {
        isSmartSelection = smart;
    }

    initializeEventListener(this);
}

CreasePatternViewer.prototype.FOLD_NONE = 1000;
CreasePatternViewer.prototype.FOLD_LINES = 1001;
CreasePatternViewer.prototype.FOLD_POINTS = 1002;
CreasePatternViewer.prototype.CREATE_CREASE = 1003;